Skip to content

fix: coalesce split Myers hunks to prevent false merge conflicts#2476

Merged
Sebastian Thiel (Byron) merged 3 commits intoGitoxideLabs:mainfrom
mtsgrd:fix/false-conflict-empty-insertion-overlap
Apr 19, 2026
Merged

fix: coalesce split Myers hunks to prevent false merge conflicts#2476
Sebastian Thiel (Byron) merged 3 commits intoGitoxideLabs:mainfrom
mtsgrd:fix/false-conflict-empty-insertion-overlap

Conversation

@mtsgrd
Copy link
Copy Markdown
Contributor

@mtsgrd Mattias Granlund (mtsgrd) commented Mar 20, 2026

Summary

  • Fixes false merge conflicts caused by imara-diff's Myers algorithm splitting a single logical change into a non-empty deletion + empty insertion separated by one unchanged base line
  • Adds coalesce_empty_insertions_with_nearest_same_side_hunk() as a pre-processing step after sorting hunks, before the intersection check
  • The coalescing re-joins split hunks by extending the nearest same-side non-empty hunk to absorb the empty insertion (gap ≤ 1 base line)
  • Includes a minimal reproduction test (false_conflict::myers_false_conflict_with_blank_line_ambiguity) — 5/4/4 lines of generic text

Context

Reported in #2475. When GitButler amends a commit via create_tree()merge_trees(), certain edits produce a CherryPickMergeConflict that git merge-file resolves cleanly.

Root cause

Three-way merge of:
base

  alpha_x
  (blank)
  bravo_x
  charlie_x
  (blank)

base → ours (delete alpha_x, add a blank, remove trailing blank):

- alpha_x
+ (blank)
  (blank)
  bravo_x
  charlie_x
- (blank)

base → theirs (delete bravo_x):

  alpha_x
  (blank)
- bravo_x
  charlie_x
  (blank)

Myers (imara-diff) diffs base→ours as three hunks:

# before after meaning
1 0..1 0..0 delete alpha_x
2 2..2 1..2 insert blank at position 2
3 4..5 4..4 delete trailing blank

Histogram diffs it as two hunks:

# before after meaning
1 0..1 0..1 replace alpha_x → blank
2 4..5 4..4 delete trailing blank

Theirs (both algorithms): before=2..3 after=2..2 (delete bravo_x).

The empty insertion at position 2 (Myers hunk #2) collides with theirs' deletion at 2..3 via the empty-range special case in left_overlaps_right, producing a false conflict.

Fix

After sorting hunks by ancestor position, a new coalesce_empty_insertions_with_nearest_same_side_hunk() pass absorbs empty insertions into their nearest preceding same-side non-empty hunk (gap ≤ 1). This turns Myers hunks #1 + #2 into a single {before=0..2, after=0..2} hunk that no longer overlaps with theirs at position 2.

The coalescing is conservative — it only fires when (a) the hunk has an empty before-range, (b) a same-side non-empty hunk exists within 1 base line, and (c) that hunk is the nearest same-side hunk. This avoids affecting cases like zdiff3-interesting where standalone empty insertions represent genuinely different changes.

Test plan

  • New test myers_false_conflict_with_blank_line_ambiguity passes
  • Full gix-merge test suite passes (22/22), including run_baseline and tree::run_baseline
  • git merge-file -p confirms clean merge on the same inputs

Closes #2475

🤖 Generated with Claude Code

@mtsgrd
Copy link
Copy Markdown
Contributor Author

Mattias Granlund (mtsgrd) commented Mar 20, 2026

Sebastian Thiel (@Byron) I think the journey test failure is flaky? Also, as far as I can tell this analysis doesn't contain any contradictions, and the .svelte based test cases pass correctly with this patch applied.

Copy link
Copy Markdown

@slarse Simon Larsén (slarse) left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello! I took the liberty of analyzing the test case and problem. See way too long comment :)

Comment thread gix-merge/tests/merge/blob/false_conflict.rs Outdated
When imara-diff's Myers algorithm diffs two files, it sometimes splits
what is logically one change into a non-empty deletion hunk and a
separate empty insertion hunk, with one unchanged base line between
them. This is a valid minimal edit script, but it differs from the
alignment that git's xdiff (also Myers-based) would choose.

When the empty insertion lands at a base position that the other side
of a 3-way merge also touches, `take_intersecting` reports an overlap
and the merge produces a conflict — even though `git merge-file`
resolves the same inputs cleanly.

Fix this by adding a pre-processing step after sorting hunks: for each
empty-range insertion hunk, look backwards for the nearest same-side
non-empty hunk within a gap of at most one unchanged base line. If
found, extend that hunk to cover the gap and the insertion point. This
re-joins the split hunk, making the merge robust to different diff
algorithm alignment choices.

The coalescing is conservative: it only applies when (a) the insertion
hunk has an empty before-range, (b) there is a same-side non-empty
hunk nearby (gap ≤ 1), and (c) that hunk is the nearest same-side
hunk. This avoids affecting cases like zdiff3-interesting where empty
insertions are standalone and represent genuinely different changes.

Closes GitoxideLabs#2475

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This can lead to cleaner, more Git-like merges.
@Byron Sebastian Thiel (Byron) force-pushed the fix/false-conflict-empty-insertion-overlap branch from 9b65de7 to 91feaf2 Compare April 19, 2026 14:04
@Byron Sebastian Thiel (Byron) marked this pull request as ready for review April 19, 2026 14:07
- move test to correct location
Comment on lines -482 to +484
hunks.extend(imara_diff::Diff::compute(algorithm, input).hunks().map(|hunk| Hunk {
let mut diff = imara_diff::Diff::compute(algorithm, input);
diff.postprocess_lines(input);
hunks.extend(diff.hunks().map(|hunk| Hunk {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is now the actual fix, which was enabled by migrating to imara-diff v0.2 which comes with this kind of post-processing.
This also was facilitated by finally vendoring it, so the entire stack for diffing can be manipulated more swiftly and easily.

@Byron
Copy link
Copy Markdown
Member

And thanks again for making this happen Mattias Granlund (@mtsgrd), and my apologies for taking this long. While ultimately taking the long route turned out to be right, it's also a bit me being afraid of these things.
Admittedly, with our new Confidants by our side, it feels easier 😁.

@Byron Sebastian Thiel (Byron) merged commit 172bd22 into GitoxideLabs:main Apr 19, 2026
56 of 58 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Blob merge with Myers algorithm produces false conflict where git merge-file resolves cleanly

3 participants